﻿using System;
using System.Collections;
using System.Reflection;

namespace Dsl.Ped.Common.Helpers
{
    /// <summary>
    /// Helper that is checking objects in the object graph.
    /// </summary>
    public class ObjectGraphHelper
    {
        /// <summary>
        /// Hashtable of all found elements
        /// </summary>
        private readonly Hashtable hashListOfFoundElements;

        private ObjectGraphHelper()
        {
            this.hashListOfFoundElements  = new Hashtable();
        }
        
        /// <summary>
        /// Check if the <see cref="element"/> is in the object graph of the <see cref="parent"/> object
        /// </summary>
        /// <param name="parent">Parent object of the graph</param>
        /// <param name="element">Element which we are verifying for its containment in the graph</param>
        /// <returns>True, if the <see cref="element"/> is in object graph of <see cref="parent"/> element</returns>
        public static bool IsInObjectGraph(object parent, object element)
        {
            var instance = new ObjectGraphHelper();
            return instance.CheckInObjectGraph(parent, element);
        }

        /// <summary>The check in object graph.</summary>
        /// <param name="parent">The parent.</param>
        /// <param name="element">The element.</param>
        private bool CheckInObjectGraph(object parent, object element)
        {
            if (parent == null || element == null || parent is ValueType || parent is string || parent is DateTime)
            {
                return false;
            }

            hashListOfFoundElements.Add(parent, true);

            if (this.CompareObjects(parent, element))
            {
                return true;
            }

            var enumerableElement = parent as IEnumerable;
            if (enumerableElement != null)
            {
                foreach (object item in enumerableElement)
                {
                    if (item is IEnumerable && !(item is string))
                    {
                        var result = this.CheckInObjectGraph(item, element);
                        if (result == true) return true;
                    }
                    else
                    {
                        if (!this.AlreadyTouched(item))
                        {
                            var result = this.CheckInObjectGraph(item, element);
                            if (result == true) return true;
                        }
                        else
                        {
                            // Bidirectional reference
                            return false;
                        }
                    }
                }
            }
            else
            {
                MemberInfo[] members = parent.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
                foreach (var memberInfo in members)
                {
                    var propertyInfo = memberInfo as PropertyInfo;

                    if (propertyInfo == null)
                    {
                        continue;
                    }

                    var type = propertyInfo.PropertyType;

                    object value = null;
                    try
                    {
                        value = propertyInfo.GetValue(parent, null);
                    } catch (Exception exc)
                    {
                        // TODO
                    }
                    if (value == null)
                    {
                        continue;
                    }

                    if (!type.IsValueType && type != typeof(string) && type != typeof(DateTime))
                    {
                        var isEnumerable = typeof(IEnumerable).IsAssignableFrom(type);

                        var alreadyTouched = !isEnumerable && this.AlreadyTouched(value);
                        if (!alreadyTouched)
                        {
                            var result = this.CheckInObjectGraph(value, element);
                            if (result == true) return true;
                        }
                        else
                        {
                            // Bidirectional reference
                            return false;
                        }
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Compare two object for the equality
        /// </summary>
        /// <param name="a">First object to be compared</param>
        /// <param name="b">Second object to be compared</param>
        /// <returns>True, if the objects are equal</returns>
        private bool CompareObjects(object a, object b)
        {
            return a.Equals(b);
        }

        /// <summary>
        /// Check if the object has already been touched 
        /// during the exploring an object graph
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        private bool AlreadyTouched(object value)
        {
            var result = hashListOfFoundElements[value];
            return (result != null);
        }
    }
}
